package com.kamistoat.meimeichat.meimeichatserverapplication.kcp;

import com.kamistoat.meimeichat.meimeichatserverapplication.netty.NettyServerBizHandler;
import com.kamistoat.meimeichat.meimeichatserverapplication.netty.service.NettyUserService;
import com.kamistoat.meimeichat.meimeichatserverapplication.netty.utils.NettyConstants;
import com.kamistoat.meimeichat.meimeichatserverapplication.netty.utils.NettySocketChannelUtil;
import com.kamistoat.meimeichatserveraggrement.protocol.Command;
import com.kamistoat.meimeichatserveraggrement.protocol.Packet;
import com.kamistoat.meimeichatserveraggrement.protocol.login.ClientOnLineStatusNotice;
import com.kamistoat.meimeichatserveraggrement.protocol.login.dto.ClientAddressDto;
import com.kamistoat.meimeichatserveraggrement.protocol.login.dto.UserFriendDto;
import com.kamistoat.meimeichatserveraggrement.protocol.p2p.TCP_EstablishP2PServer2Client;
import com.kamistoat.meimeichatserveraggrement.protocol.p2p.UDP_AnswerP2PClient2Server;
import com.kamistoat.meimeichatserveraggrement.protocol.p2p.UDP_EstablishP2PClient2Server;
import com.kamistoat.meimeichatserveraggrement.util.SerializationUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.DatagramPacket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import java.net.InetSocketAddress;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * 服务端UDP链接Handler。用于协调两个客户端之间的打洞链接
 */
@Component
@ChannelHandler.Sharable
public class P2PDatagramHandlerServer extends SimpleChannelInboundHandler<DatagramPacket> {

    private final Logger logger = LoggerFactory.getLogger(P2PDatagramHandlerServer.class);

    @Autowired
    NettyUserService nettyUserService;

    @Qualifier(value = "nettyThreadPoolExecutor")
    @Autowired
    ThreadPoolExecutor nettyThreadPoolExecutor;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception {
        InetSocketAddress clientP2PAddress = packet.sender();
        ByteBuf buf = (ByteBuf) packet.copy().content();

        // 因为前4位是数据长度
        if (buf.readableBytes() < 4) {
            logger.error("收到来自 {} 的UDP链接，但无法解析内容", clientP2PAddress);
            return;
        }

        buf.markReaderIndex();
        int dataLength = buf.readInt();
        if (buf.readableBytes() < dataLength) {
            buf.resetReaderIndex();
            return;
        }

        // 读取UDP报文中首位标志位，判断消息对象
        byte command = buf.readByte();
        // 读取剩下的内容，跳过首位对象标识符
        byte[] req = new byte[dataLength - 1];
        buf.readBytes(req);

        /**
         * 普通String信息
         */
        if (command == Command.NormalString) {
            String notice = new String(req, "UTF-8");
            logger.info(notice);
        }
        /**
         * Command.UDP_ClientOnlineNotice:
         * Client登录成功(包括重连)后发送的UDP注册事件。客户端注册该UDP给服务器，并告知所有在线好友其UDP地址
         */
        else if (command == Command.UDP_ClientOnlineNotice) {
            String newUserId = new String(req, "UTF-8");
            NettySocketChannelUtil.USER_ID_P2P_ADDRESS_MAP.put(newUserId, clientP2PAddress);
            logger.info("收到 {} 用户的登录UDP链接, 缓存其UDP打洞地址: {}, 开始通知其所有在线好友", newUserId, clientP2PAddress.toString());

            byte[] resBytes = "服务端已缓存自身UDP打洞地址".getBytes();
            ByteBuf resBuf = Unpooled.buffer();
            // 添加数据长度，占4位. 加1是因为存在首位的标识符
            resBuf.writeInt(resBytes.length + 1);
            // 添加首位标识，标识消息类型
            resBuf.writeByte(Command.NormalString);
            resBuf.writeBytes(resBytes);
            ctx.writeAndFlush(new DatagramPacket(resBuf, clientP2PAddress));

            // 异步向所有在线用户广播其UDP地址
            nettyThreadPoolExecutor.execute(() -> noticeAllOnLineFriend(clientP2PAddress, newUserId));
        }
        /**
         * Command.UDP_EstablishP2PClient2Server:
         * ClientA请求服务器协助建立与ClientB的P2P链接
         */
        else if (command == Command.UDP_EstablishP2PClient2Server) {
            UDP_EstablishP2PClient2Server udp_establishP2PClient2Server =
                    (UDP_EstablishP2PClient2Server) SerializationUtil.deserialize(req, Packet.get(command));
            String selfUserId = udp_establishP2PClient2Server.getSelfClientUserId();
            String targetUserId = udp_establishP2PClient2Server.getTargetClientUserId();
            logger.info("用户 {} 请求与用户 {} 建立P2P链接", selfUserId, targetUserId);
            // 则携带A的UDP地址通知B
            establishP2PServer2Client(clientP2PAddress, selfUserId, targetUserId);
            logger.info("服务器已通知用户 {} 建立到用户 {} 的NET映射", targetUserId, selfUserId);
        }
        /**
         * ClientB通知服务器，B->A直连已建立，但尚且不知道A->B的状态
         */
        else if(command == Command.UDP_AnswerP2PClient2Server) {
            UDP_AnswerP2PClient2Server udp_answerP2PClient2Server =
                    (UDP_AnswerP2PClient2Server) SerializationUtil.deserialize(req, Packet.get(command));
            String selfUserId = udp_answerP2PClient2Server.getSelfClientUserId();
            String targetUserId = udp_answerP2PClient2Server.getTargetClientUserId();
            logger.info("用户 {} 已建立自身到用户 {} 的NET映射，但尚且不知道反向映射是否成功", selfUserId, targetUserId);
            // 则携带B的UDP地址通知A
            establishP2PServer2Client(clientP2PAddress, selfUserId, targetUserId);
            logger.info("服务器已通知用户 {} 建立到用户 {} 的NET映射", targetUserId, selfUserId);
        }
    }

    /**
     * 客户端Y请求服务端通知客户端X建立与自身的P2P打洞。
     * @param clientP2PAddress Y的UDP地址
     * @param selfUserId Y的userId
     * @param targetUserId X的userId，据此找出X的信道
     */
    private void establishP2PServer2Client(InetSocketAddress clientP2PAddress, String selfUserId, String targetUserId) {
        // 如果UDP地址发生改变，顺便异步通知一下它的所有在线好友
        if (!NettySocketChannelUtil.USER_ID_P2P_ADDRESS_MAP.containsKey(selfUserId) ||
                !NettySocketChannelUtil.USER_ID_P2P_ADDRESS_MAP.get(selfUserId).equals(clientP2PAddress)) {
            logger.info("用户 {} 的UDP地址发生改变，更新缓存并重新通知其在线好友", selfUserId);
            NettySocketChannelUtil.USER_ID_P2P_ADDRESS_MAP.put(selfUserId, clientP2PAddress);
            nettyThreadPoolExecutor.execute(() -> noticeAllOnLineFriend(clientP2PAddress, selfUserId));
        }

        // 服务器携带Y的UDP地址通知X
        String targetUserHost = clientP2PAddress.toString().split(":")[0].substring(1);
        Integer targetUserPort = Integer.valueOf(clientP2PAddress.toString().split(":")[1]);
        TCP_EstablishP2PServer2Client tcp_establishP2PServer2Client = new TCP_EstablishP2PServer2Client(selfUserId, targetUserHost, targetUserPort);
        Channel channel = NettySocketChannelUtil.getChannel(targetUserId);
        if (channel != null) {
            channel.writeAndFlush(tcp_establishP2PServer2Client);
        }
    }

    /**
     * 向 newUserId 的所有在线用户广播其UDP地址
     *
     * @param clientP2PAddress newUserId的UDP地址
     * @param newUserId        userId
     */
    private void noticeAllOnLineFriend(InetSocketAddress clientP2PAddress, String newUserId) {
        // 向该用户的所有在线好友发送通知，令它们缓存该用户的P2P地址。
        ClientOnLineStatusNotice onLineStatusNotice = new ClientOnLineStatusNotice();
        onLineStatusNotice.setOnLineStatus(NettyConstants.OnLineStatus.OnLine.getCode());
        onLineStatusNotice.setFriendId(newUserId);
        ClientAddressDto clientAddressDto = new ClientAddressDto();
        clientAddressDto.setWantedUserId(newUserId);
        clientAddressDto.setWantedUserHost(clientP2PAddress.toString().split(":")[0].substring(1));
        clientAddressDto.setWantedUserPort(Integer.parseInt(clientP2PAddress.toString().split(":")[1]));
        onLineStatusNotice.setClientAddressDto(clientAddressDto);

        // 向所有在线好友发送通知
        for (UserFriendDto userFriendDto : nettyUserService.queryUserFriendInfoList(newUserId)) {
            Channel onLineFriendChannel = NettySocketChannelUtil.getChannel(userFriendDto.getFriendId());
            if (onLineFriendChannel != null) {
                onLineFriendChannel.writeAndFlush(onLineStatusNotice);
            }
        }
        logger.info("已尽力通知 " + newUserId + " 的所有在线好友");
    }
}
